/**
 ** 相机导航器：控制相机的移动位置
 **
 ** 功能
 **   - 移动动画
 **   - OrbitControl 目标点控制
 **   - 移动到物体（最近距离）
 **   - 获取相机位置，以及 OrbitControl.target
 **   - 移动到物体正前方
 **
 */

import { THREE, OrbitControls, gsap } from "@/scripts/three/toolchain";

import { boundingCalculator } from "./BoundingCalculator";

export type Number5 = [number, number, number, number, number];
export type Number3 = [number, number, number];

// 提取库函数
function r2(n: number) {
  return Math.round(n * 100) / 100;
}
// 复用变量
const box3 = new THREE.Box3();
// 常量
const DEFAULT_DURATION = 1;

export default class CameraNavigator {
  // 相机和控制器
  camera: THREE.PerspectiveCamera;
  controls?: OrbitControls;
  constructor(camera: THREE.PerspectiveCamera, controls?: OrbitControls) {
    this.camera = camera;
    if (controls) this.controls = controls;
  }

  // 设置相机位置
  setCameraPos(cameraPos: Number5) {
    const [px, py, pz, rx, ry] = cameraPos;
    this.camera.position.set(px, py, pz);
    this.camera.rotation.set(rx, ry, 0);
  }

  // 设置轨道控制器目标位置
  setOrbitControlsTarget(targetPos: Number3) {
    if (this.controls == null) return;

    const [px, py, pz] = targetPos;
    this.controls.target.set(px, py, pz);
  }

  // 相机飞行到
  async flyCameraTo(
    x: number,
    y: number,
    z: number,
    rx: number,
    ry: number,
    duration = DEFAULT_DURATION,
    ease = "sine.inOut"
  ) {
    return new Promise((resolve) => {
      setTimeout(resolve, duration * 1000);
      const gsapOptions = {
        duration,
        ease,
      };
      gsap.to(this.camera.rotation, {
        x: rx,
        y: ry,
        z: 0,
        ...gsapOptions,
        onUpdate() {
          // Hack: 解决相机飞行动画过程中绑定弹窗位置漂移的问题
          // 但要注意：一定要先 controls.update() 再 syncMarkerModalPos() 否则还会漂移
          if (this.controls) this.controls.update();
        },
      });
      gsap.to(this.camera.position, {
        x: x,
        y: y,
        z: z,
        ...gsapOptions,
      });
    });
  }

  // OrbitControls 的目标飞行到
  async flyControlsTargetTo(
    x: number,
    y: number,
    z: number,
    duration = DEFAULT_DURATION,
    ease: string = "sine.inOut"
  ) {
    if (this.controls == null) return;

    return new Promise((resolve) => {
      setTimeout(resolve, duration * 1000);
      const gsapOptions = {
        duration,
        ease: ease,
      };
      gsap.to((this.controls as OrbitControls).target, {
        x: x,
        y: y,
        z: z,
        ...gsapOptions,
      });
    });
  }

  // 飞行相机到物体（包围盒）
  async flyCameraToObject(object: THREE.Object3D, distFactor = 2) {
    const objectCenter = new THREE.Vector3();
    const objectSize = new THREE.Vector3();
    box3.setFromObject(object);
    box3.getCenter(objectCenter);
    box3.getSize(objectSize);
    const objectRadius = objectSize.length();
    // 1. 计算当前相机位置到物体几何中心的向量
    const cameraObjectVec = new THREE.Vector3();
    cameraObjectVec.copy(objectCenter).sub(this.camera.position);
    // 2. 设置向量长度为目标对象包围球的 n 倍
    cameraObjectVec.setLength(objectRadius * distFactor);
    // 3. 计算相机目标位置为“目标物体位置 - 上个向量”
    const targetCameraPos = objectCenter.clone().sub(cameraObjectVec);
    // 4. 限制视角最低高度为物体几何中心，再加 1/10 物体包围球半径
    const minAlt = objectCenter.y + objectRadius / 10;
    if (targetCameraPos.y < minAlt) targetCameraPos.y = minAlt;
    // 5. 开始动画
    gsap.to(this.camera.position, {
      x: targetCameraPos.x,
      y: targetCameraPos.y,
      z: targetCameraPos.z,
      ease: "power2.inOut",
      duration: DEFAULT_DURATION,
    });
    if (this.controls) {
      gsap.to(this.controls.target, {
        x: objectCenter.x,
        y: objectCenter.y,
        z: objectCenter.z,
        ease: "power2.inOut",
        duration: DEFAULT_DURATION,
      });
      // controls.update()
    }
  }

  // 飞行相机到物体正前方一定距离处
  // [x] 考虑物体有变换
  // [x] 考虑物体是子节点
  // [x] 考虑物体包围盒偏移
  // [x] 考虑没有轨道控制器时相机的旋转问题
  // 注意：为目标物体添加子物体，会扩展其包围盒，而导致位置不对
  async flyCameraToFront(object: THREE.Object3D, distance: number = 4) {
    // 物体包围盒计算
    boundingCalculator.calculateWithoutTransform(object);
    const objectOriginCenterOffset = boundingCalculator.center;
    // 物体位置
    const objectPosition = object.position
      .clone()
      .add(objectOriginCenterOffset);
    // 考虑层级，转为世界坐标
    if (object.parent) objectPosition.applyMatrix4(object.parent.matrixWorld);
    // 物体朝向（向量）
    const objectOrient = new THREE.Vector3(0, 0, 1);
    object.getWorldDirection(objectOrient);
    const theta = Math.atan2(objectOrient.x, objectOrient.z);
    // 物体前某点位置
    objectOrient.setLength(distance);
    const objectFrontPoint = objectPosition.clone().add(objectOrient);
    this.flyCameraTo(
      objectFrontPoint.x,
      objectFrontPoint.y,
      objectFrontPoint.z,
      0,
      theta
    );
    this.flyControlsTargetTo(
      objectPosition.x,
      objectPosition.y,
      objectPosition.z
    );
  }

  // 飞行相机到物体正前方一定距离处，但看像后面（而非物体），用于模拟摄像机视角模拟
  async flyCameraToFPS(object: THREE.Object3D, frontDist = 1, orbitDist = 2) {
    // 物体位置
    const objectPosition = object.position.clone();
    // 考虑层级，转为世界坐标
    if (object.parent) objectPosition.applyMatrix4(object.parent.matrixWorld);
    // 物体朝向（向量）
    const objectOrient = new THREE.Vector3(0, 0, 1);
    object.getWorldDirection(objectOrient);
    const theta = Math.atan2(objectOrient.x, objectOrient.z);
    // 物体前某点位置
    objectOrient.setLength(frontDist);
    const objectFrontPoint = objectPosition.clone().add(objectOrient);
    objectOrient.setLength(orbitDist);
    const objectFrontTargetPoint = objectFrontPoint.clone().add(objectOrient);
    this.flyCameraTo(
      objectFrontPoint.x,
      objectFrontPoint.y,
      objectFrontPoint.z,
      0,
      theta
    );
    this.flyControlsTargetTo(
      objectFrontTargetPoint.x,
      objectFrontTargetPoint.y,
      objectFrontTargetPoint.z
    );
  }

  // 飞行相机到物体正前方（距离根据包围盒计算）
  async flyCameraToFrontAutoDist(object: THREE.Object3D) {
    // TODO
  }

  // 在控制台输出相机位置
  logCameraPos() {
    const p = this.camera.position;
    const r = this.camera.rotation;
    const value = `[${r2(p.x)}, ${r2(p.y)}, ${r2(p.z)}, ${r2(r.x)}, ${r2(
      r.y
    )}]`;
    // console.log(`当前相机点位：${value}`)
    return value;
  }

  // 在控制台输出轨道控制器的目标位置
  logControlsTargetPos() {
    if (this.controls == null) return;

    const p = this.controls.target;
    const value = `[${r2(p.x)}, ${r2(p.y)}, ${r2(p.z)}]`;
    // console.log(`OrbitControls.target: ${value}`)
    return value;
  }
}
